﻿using System;
using System.Collections.Generic;
using System.Linq;
using Myotragus.Data.Domain;
using Myotragus.Data.Util;
using NHibernate;
using NHibernate.Criterion;
using NHibernate.SqlCommand;
using NHibernate.Transform;

namespace Myotragus.Data
{
  public abstract class LocalizableRepository<TId, TEntity, TSaveEntity, TLoadEntity> :
    Repository<TEntity>, ILocalizableRepository<TId, TEntity>
    where TEntity : class, IEntity<TId>
    where TSaveEntity : class, IEntity<TId>, new()
    where TLoadEntity : class, IEntity<TId>
  {
    #region Dependencies

    public ILocalizableEntityDescriptor<TEntity, TSaveEntity, TLoadEntity> Descriptor
    {
      get;
      private set;
    }
    
    public ILocalizableEntityProjectionBuilder<TEntity, TSaveEntity, TLoadEntity> ProjectionsBuilder
    {
      get;
      private set;
    }

    protected IProjection Projection
    {
      get;
      private set;
    }

    protected LocalizableRepository(ILocalizableEntityDescriptor<TEntity, TSaveEntity, TLoadEntity> descriptor,
      ILocalizableEntityProjectionBuilder<TEntity, TSaveEntity, TLoadEntity> projectionsBuilder)
    {
      Descriptor = descriptor;
      ProjectionsBuilder = projectionsBuilder;
      Projection = ProjectionsBuilder.BuildProjection();
    }

    protected static readonly Type TypeOfTLoadEntity = typeof(TLoadEntity);
    protected static readonly Type TypeOfTSaveEntity = typeof(TSaveEntity);
    #endregion

    #region Abstracts

    protected virtual bool IsUpdate(TId id)
    {
      return !Equals(id, default(TId));
    }

    #endregion

    #region Contants

    protected const string StrLocalizableTextTable = "LocalizableText";

    #endregion

    #region Helpers

    protected virtual void AttachAliases(ICriteria criteria, int lcId, JoinType joinType)
    {
      foreach (var join in Descriptor.LocalizableProperties)
      {
        var restrictionLcId = string.Format("{0}.LcId", join);
        criteria.CreateAlias(join, join, joinType, Restrictions.Eq(restrictionLcId, lcId));
      }
    }

    protected TSaveEntity CreateAndInitializeSaveEntity(ISession session, TEntity obj, int lcId)
    {
      var ops = IsUpdate(obj.Id) ? GetSaveEntity(session, obj.Id) : CreateNewSaveEntity(obj);

      foreach (var prop in Descriptor.LocalizableProperties)
      {
        var arrEmpty = ReflectionHelper.ArrObjEmpty;
        var value = (string)Descriptor.EntityProperties[prop].GetValue(obj, arrEmpty);
        var text = (ILocalizableText)Descriptor.SaveEntityProperties[prop].GetValue(ops, arrEmpty);
        text.Values[lcId] = value;
      }

      return ops;
    }

    protected virtual TSaveEntity CreateNewSaveEntity(TEntity prototype)
    {
      var result = new TSaveEntity();

      ReflectionHelper.InitializeObject(result, prototype);

      foreach (var name in Descriptor.LocalizableProperties)
      {
        var prop = Descriptor.SaveEntityProperties[name];
        var text = Activator.CreateInstance(prop.PropertyType);
        prop.SetValue(result, text, ReflectionHelper.ArrObjEmpty);
      }

      return result;
    }

    protected TSaveEntity GetSaveEntity(ISession session, TId id)
    {
      return session.Get<TSaveEntity>(id);
    }

    protected ICriteria CreateCriteria(ISession session, int lcId, int firstResult, int maxResults,
      IEnumerable<Order> orderBy)
    {
      var criteria = session.CreateCriteria(TypeOfTLoadEntity)
        .SetProjection(Projection)
        .SetResultTransformer(Transformers.AliasToBean(EntityImplementation));

      AttachAliases(criteria, lcId, JoinType.LeftOuterJoin);
      CriteriaHelper.AddOrder(criteria, orderBy);

      criteria.SetFirstResult(firstResult);
      criteria.SetMaxResults(maxResults);

      return criteria;
    }

    protected void AttachMatchingCriteria(ICriteria criteria, object pattern)
    {
      foreach (var prop in ReflectionHelper.GetProperties(pattern))
      {
        if (!Descriptor.EntityProperties.ContainsKey(prop.Name))
          continue;

        var value = prop.GetValue(pattern, ReflectionHelper.ArrObjEmpty);
        var property = Descriptor.LocalizableProperties.Contains(prop.Name)
          ? Property.ForName(string.Format("{0}.Value", prop.Name))
          : Property.ForName(prop.Name);

        criteria.Add(property.Eq(value));
      }
    }

    protected virtual void AttachLikeCriteria(ICriteria criteria, object pattern)
    {
      foreach (var prop in ReflectionHelper.GetProperties(pattern))
      {
        if (!Descriptor.EntityProperties.ContainsKey(prop.Name))
          continue;

        var value = prop.GetValue(pattern, ReflectionHelper.ArrObjEmpty);
        var property = Descriptor.LocalizableProperties.Contains(prop.Name)
          ? Property.ForName(string.Format("{0}.Value", prop.Name))
          : Property.ForName(prop.Name);

        criteria.Add(prop.PropertyType == typeof(string)
          ? property.Like(value.ToString(), MatchMode.Anywhere)
          : property.Eq(value));
      }
    }

    #endregion

    #region Read Operations

    public virtual bool Exists(TId id)
    {
      using (var session = ServicesContainer.OpenSession())
      {
        return session.CreateCriteria(TypeOfTLoadEntity)
          .Add(Property.ForName("Id").Eq(id))
          .SetProjection(Projections.RowCount()).UniqueResult<int>() > 0;
      }
    }

    public virtual bool Exists(int lcId, TId id)
    {
      using (var session = ServiceContainer.OpenSession())
      {
        var criteria = session.CreateCriteria(TypeOfTLoadEntity)
          .Add(Property.ForName("Id").Eq(id))
          .SetProjection(Projections.RowCount());

        AttachAliases(criteria, lcId, JoinType.InnerJoin);

        return criteria.UniqueResult<int>() > 0;
      }
    }

    public virtual TEntity Load(int lcId, TId id)
    {
      return FindMatching(lcId, new { Id = id }, 0, 1).FirstOrDefault();
    }

    public virtual IEnumerable<TEntity> FindAll(int lcId, params Order[] orderBy)
    {
      return FindAll(lcId, 0, -1, orderBy);
    }

    public virtual IEnumerable<TEntity> FindAll(int lcId, int firstResult, int maxResults, params Order[] orderBy)
    {
      using (var session = ServiceContainer.OpenSession())
        return CreateCriteria(session, lcId, firstResult, maxResults, orderBy).List().Cast<TEntity>();
    }

    public virtual int Count()
    {
      using (var session = ServiceContainer.OpenSession())
      {
        return session.CreateCriteria(TypeOfTLoadEntity)
          .SetProjection(Projections.RowCount()).UniqueResult<int>();
      }
    }

    public IEnumerable<TEntity> FindMatching(int lcId, object pattern, params Order[] orderBy)
    {
      return FindMatching(lcId, pattern, 0, -1, orderBy);
    }

    public virtual IEnumerable<TEntity> FindMatching(int lcId, object pattern, int firstResult,
      int maxResults, params Order[] orderBy)
    {
      using (var session = ServiceContainer.OpenSession())
      {
        var criteria = CreateCriteria(session, lcId, firstResult, maxResults, orderBy);
        AttachMatchingCriteria(criteria, pattern);

        return criteria.List().Cast<TEntity>();
      }
    }

    public virtual int CountMatching(int lcId, object pattern)
    {
      using (var session = ServiceContainer.OpenSession())
      {
        var criteria = session.CreateCriteria(TypeOfTLoadEntity).SetProjection(Projections.RowCount());

        AttachAliases(criteria, lcId, JoinType.LeftOuterJoin);
        AttachMatchingCriteria(criteria, pattern);

        return criteria.UniqueResult<int>();
      }
    }

    public IEnumerable<TEntity> FindLike(int lcId, object pattern, params Order[] orderBy)
    {
      return FindLike(lcId, pattern, 0, -1, orderBy);
    }

    public virtual IEnumerable<TEntity> FindLike(int lcId, object pattern, int firstResult,
      int maxResults, params Order[] orderBy)
    {
      using (var session = ServiceContainer.OpenSession())
      {
        var criteria = CreateCriteria(session, lcId, firstResult, maxResults, orderBy);
        AttachLikeCriteria(criteria, pattern);

        return criteria.List().Cast<TEntity>();
      }
    }

    public virtual int CountLike(int lcId, object pattern)
    {
      using (var session = ServiceContainer.OpenSession())
      {
        var criteria = session.CreateCriteria(TypeOfTLoadEntity).SetProjection(Projections.RowCount());

        AttachAliases(criteria, lcId, JoinType.LeftOuterJoin);
        AttachLikeCriteria(criteria, pattern);

        return criteria.UniqueResult<int>();
      }
    }

    #endregion

    #region Write Operations

    public void Save(int lcId, params TEntity[] objs)
    {
      Save(lcId, (IEnumerable<TEntity>)objs);
    }

    public virtual void Save(int lcId, IEnumerable<TEntity> objs)
    {
      using (var session = ServiceContainer.OpenSession())
      {
        foreach (var obj in objs)
        {
          var ops = CreateAndInitializeSaveEntity(session, obj, lcId);

          using (var transaction = session.BeginTransaction())
          {
            session.SaveOrUpdate(ops);
            transaction.Commit();

            // If operation was an insert, we need to copy the Id to the Entity instance
            obj.Id = ops.Id;
          }
        }

        session.Flush();
      }
    }

    public void Delete(params TEntity[] objs)
    {
      Delete((IEnumerable<TEntity>)objs);
    }

    public virtual void Delete(IEnumerable<TEntity> objs)
    {
      using (var session = ServiceContainer.OpenSession())
      {
        foreach (var ops in objs.Select(x => GetSaveEntity(session, x.Id)))
          using (var transaction = session.BeginTransaction())
          {
            session.Delete(ops);
            transaction.Commit();
          }

        session.Flush();
      }
    }

    #endregion
  }
}